/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.myfaces.extensions.cdi.forge.api;

import org.apache.myfaces.extensions.cdi.core.api.security.AbstractAccessDecisionVoter;
import org.apache.myfaces.extensions.cdi.forge.api.support.*;
import org.jboss.forge.parser.JavaParser;
import org.jboss.forge.parser.java.JavaClass;
import org.jboss.forge.project.Project;
import org.jboss.forge.project.facets.JavaSourceFacet;
import org.jboss.forge.project.facets.WebResourceFacet;
import org.jboss.forge.project.facets.events.InstallFacets;
import org.jboss.forge.project.services.ResourceFactory;
import org.jboss.forge.resources.*;
import org.jboss.forge.shell.*;
import org.jboss.forge.shell.plugins.*;

import javax.enterprise.event.Event;
import javax.inject.Inject;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author Cosmin Martinconi, 24.06.2011
 */
@RequiresProject
@RequiresFacet({JavaSourceFacet.class, WebResourceFacet.class})
@Alias("codi")
public class CodiPlugin implements Plugin {

    private final Project project;
    private final Event<InstallFacets> installFacets;
    private final ShellPrintWriter out;
    private final Shell shell;

    private static DefaultFolder defaultFolder;

    @Inject
    private ResourceFactory factory;

    @Inject
    public CodiPlugin(final Project project, final Event<InstallFacets> event, final ShellPrintWriter writer,
                      final Shell shell) {
        this.project = project;
        this.installFacets = event;
        this.out = writer;
        this.shell = shell;
    }

    @DefaultCommand
    public void status(final PipeOut out) {
        if (project.hasFacet(CodiFacet.class)) {
            out.println("Codi is installed.");
        } else {
            out.println("Codi is not installed. Use 'codi setup' to get started.");
        }
    }

    @Command(value = "setup", help = "Install Codi into the current project.")
    public void setup(final PipeOut out) {
        if (project.hasFacet(CodiFacet.class)) {
            ShellMessages.success(out, "Codi is configured.");
        } else {
            installFacets.fire(new InstallFacets(CodiFacet.class));
        }
    }

    @Command(value = "viewconfig-create-folder", help = "Creates a new folder and associated view configuration")
    public void viewConfigCreateFolder(
            @Option(name = "name",
                    description = "The view configuration folder name",
                    required = true) final String name,
            @Option(name = "in",
                    description = "The containing ViewConfig name",
                    required = false) final String inFileName,
            @Option(name = "default",
                    description = "Sets this ViewConfig as the default folder",
                    flagOnly = true,
                    required = false) final boolean isDefault,
            final PipeOut out) {

        if (!isInstalled()) {
            return;
        }

        //Create/Edit ViewConfig file
        ViewConfigUtil vcUtil;
        ViewConfigMetadata parentMetadata;
        ViewConfigMetadata newMetadata = null;
        if (inFileName == null) {
            if (defaultFolder != null) {
                vcUtil = defaultFolder.getDefaultViewConfig();
                parentMetadata = defaultFolder.getDefaultMetadata();
            } else {
                List<ViewConfigUtil> vcUtilList = ViewConfigUtil.getAllViewConfigs(shell);
                if (vcUtilList.isEmpty()) {
                    vcUtil = ViewConfigUtil.getNewViewConfig(shell, name);
                    parentMetadata = vcUtil.getViewConfigMetadata();
                    newMetadata = parentMetadata;
                    vcUtil.saveViewConfig();
                } else if (1 == vcUtilList.size()) {
                    vcUtil = vcUtilList.get(0);
                    parentMetadata = vcUtil.getViewConfigMetadata();
                } else {
                    ShellMessages.error(out, "No default ViewConfig folder found. Try again using the [--in] option.");
                    return;
                }
            }
        } else {
            String viewConfigName = inFileName;
            if (inFileName.contains(".")) {
                viewConfigName = inFileName.substring(0, inFileName.indexOf("."));
            }
            vcUtil = new ViewConfigUtil(shell, viewConfigName);
            parentMetadata = vcUtil.getViewConfigMetadata(inFileName);
        }

        if (newMetadata == null) {
            newMetadata = parentMetadata.getNewFolder(name);
            parentMetadata.addMetadata(newMetadata);
            vcUtil.saveViewConfig();
        }


        if (newMetadata.getAssociatedFile().mkdirs()) {
            ShellMessages.info(out, "[" + newMetadata.getAssociatedFile() + "] directory created successful.");
        }

        if (isDefault) {
            defaultFolder = new DefaultFolder(vcUtil, newMetadata);
            ShellMessages.info(out, "ViewConfig [" + name + "] configured as default folder");
        }
    }

    @Command(value = "viewconfig-create-page", help = "Creates a new page and associated view configuration")
    public void viewConfigCreatePage(
            @Option(name = "name",
                    description = "The view configuration page name",
                    required = true) final String name,
            @Option(name = "in",
                    description = "The containing ViewConfig folder name",
                    required = false) final String inFileName,
            @Option(name = "errorPage",
                    description = "Configures the new page as an ErrorPage",
                    flagOnly = true,
                    required = false) final boolean isErrorPage,
            final PipeOut out) {

        if (!isInstalled()) {
            return;
        }

        //Edit ViewConfig file
        ViewConfigUtil vcUtil;
        ViewConfigMetadata parentMetadata;
        if (inFileName == null) {
            if (defaultFolder != null) {
                vcUtil = defaultFolder.getDefaultViewConfig();
                parentMetadata = defaultFolder.getDefaultMetadata();
            } else {
                List<ViewConfigUtil> vcUtilList = ViewConfigUtil.getAllViewConfigs(shell);
                if (1 == vcUtilList.size()) {
                    vcUtil = vcUtilList.get(0);
                    parentMetadata = vcUtil.getViewConfigMetadata();
                } else {
                    ShellMessages.error(out, "No default ViewConfig folder found. Try again using the [--in] option.");
                    return;
                }
            }
        } else {
            String viewConfigName = inFileName;
            if (inFileName.contains(".")) {
                viewConfigName = inFileName.substring(0, inFileName.indexOf("."));
            }
            vcUtil = new ViewConfigUtil(shell, viewConfigName);
            parentMetadata = vcUtil.getViewConfigMetadata(inFileName);
        }

        ViewConfigMetadata newMetadata = parentMetadata.getNewPage(name, isErrorPage);
        parentMetadata.addMetadata(newMetadata);

        vcUtil.saveViewConfig();

        //create required view page
        FileResource page = new UnknownFileResource(factory, newMetadata.getAssociatedFile());
        page.createNewFile();

        InputStream viewTemplate = getViewTemplate(newMetadata.getFileName());
        if (viewTemplate != null) {
            page.setContents(viewTemplate);
        }
    }

    @Command(value = "viewconfig-create-config", help = "Creates a new ViewConfig")
    public void viewConfigCreateConfig(
            @Option(name = "name",
                    description = "The ViewConfig name",
                    required = true) final String name,
            @Option(name = "voter",
                    description = "The new Voter name",
                    required = false) final String voterName,
            @Option(name = "default",
                    description = "Sets this ViewConfig as the default folder",
                    flagOnly = true,
                    required = false) final boolean isDefault,
            final PipeOut out) throws FileNotFoundException {

        if (!isInstalled()) {
            return;
        }

        JavaSourceFacet javaFacet = project.getFacet(JavaSourceFacet.class);

        //Create ViewConfig and Voter files
        ViewConfigUtil vcUtil = ViewConfigUtil.getNewViewConfig(shell, name);
        if (voterName != null) {
            JavaClass voterSource = JavaParser.create(JavaClass.class)
                                               .setPackage(javaFacet.getBasePackage())
                                               .setName(voterName)
                                               .setSuperType(AbstractAccessDecisionVoter.class);
            javaFacet.saveJavaSource(voterSource);
            vcUtil.getViewConfigMetadata()
                  .setSecured(voterName, voterSource.getQualifiedName());
        }

        vcUtil.saveViewConfig();

        if (isDefault) {
            defaultFolder = new DefaultFolder(vcUtil, vcUtil.getViewConfigMetadata());
            ShellMessages.info(out, "ViewConfig [" + name + "] configured as default folder");
        }
    }

    @Command(value = "viewconfigMetadata", help = "Edits a specific ViewConfig metadata")
    public void viewConfigMetadata(
            @Option(name = "name",
                    description = "The ViewConfig metadata name",
                    completer = ViewConfigCompleter.class,
                    required = true) final String name,
            @Option(name = "default",
                    description = "Sets this metadata as the default folder",
                    flagOnly = true,
                    required = false) final boolean isDefault,
            @Option(name = "redirect",
                    description = "Adds redirect navigation mode",
                    flagOnly = true,
                    required = false) final boolean redirect,
            @Option(name = "extendWith",
                    description = "Extends the metadata with the given ViewConfig",
                    required = false) final String extendWith,
            final PipeOut out) {

        if (!isInstalled()) {
            return;
        }

        //Edit ViewConfig file
        String viewConfigName = name;
        if (name.contains(".")) {
            viewConfigName = name.substring(0, name.indexOf("."));
        }
        ViewConfigUtil vcUtil = new ViewConfigUtil(shell, viewConfigName);
        ViewConfigMetadata vcMetadata = vcUtil.getViewConfigMetadata(name);

        if (redirect) {
            vcMetadata.setRedirectNavigation();
        }

        if (isDefault) {
            if (vcMetadata.isFolder()) {
                defaultFolder = new DefaultFolder(vcUtil, vcMetadata);
                ShellMessages.info(out, "Metadata [" + name + "] configured as default folder");
            } else {
                ShellMessages.error(out, "[" + name + "] metadata is a page, [--default] can not be applied.");
            }
        }

        if (extendWith != null) {
            vcMetadata.addExtensionInterface(new ViewConfigUtil(shell, extendWith).getViewConfigMetadata());
        }

        vcUtil.saveViewConfig();
    }

    @Command(value = "viewconfig-list-all", help = "List all ViewConfig folders and pages")
    public void viewConfigListAll(final PipeOut out) {
        if (!isInstalled()) {
            return;
        }

        //Find required ViewConfig file
        ViewConfigUtil vcUtil = new ViewConfigUtil(shell);
        ViewConfigMetadata vcRes = vcUtil.getViewConfigMetadata();

        System.out.println();
        printViewConfig(Arrays.asList(vcRes), false, "", false, false);
    }

    @Command(value = "viewconfig-list-pages", help = "List all ViewConfig pages")
    public void viewConfigListPages(final PipeOut out) {
        if (!isInstalled()) {
            return;
        }

        //Find required ViewConfig file
        ViewConfigUtil vcUtil = new ViewConfigUtil(shell);
        ViewConfigMetadata vcRes = vcUtil.getViewConfigMetadata();

        System.out.println();
        printViewConfig(Arrays.asList(vcRes), true, "", false, false);
    }

    /**
     * The ViewConfig recursive print method, that lists all/pages configuration within a ViewConfig file.
     *
     * @param metadataList    The metadata child list of a given metadata. The list will recursively cover
     *                        all ViewConfig's resources. The initial value will contain the root ViewConfigMetadata.
     * @param pagesOnly       The flag that enables the printing of pages only.
     * @param parentName      The name of the parent metadata.
     * @param parentRedirect  The redirect property inherited from the parent metadata.
     * @param parentSecured   The secured property inherited from the parent metadata.
     */
    private void printViewConfig(List<ViewConfigMetadata> metadataList, boolean pagesOnly, String parentName, boolean parentRedirect, boolean parentSecured) {

        for (ViewConfigMetadata res : metadataList) {
            boolean secured = false;
            boolean redirect;
            boolean errPage = false;

            if (res.isSecured()) {
                secured = true;
            } else {
                for (String extension : res.getExtensionClasses()) {
                    try {
                        if (new ViewConfigUtil(shell, extension).getViewConfigMetadata().isSecured()) {
                            secured = true;
                        }
                    } catch (ViewConfigException ignore) {}
                }
            }
            redirect = res.isRedirectNavigation();

            String namePath = parentName;
            if (namePath.isEmpty()) {
                namePath = res.getName();
            } else {
                namePath = namePath + "." + res.getName();
            }

            if (!pagesOnly || !res.isFolder()) {
                boolean attrAdded = false;
                System.out.print(namePath);
                System.out.print(" extends " + res.getExtensionClasses());
                System.out.print(" (");
                if (secured) {
                    System.out.print("secured");
                    attrAdded = true;
                }
                if (redirect) {
                    System.out.print(attrAdded ? ", redirect" : "redirect");
                    attrAdded = true;
                }
                if (!res.isFolder()) {
                    errPage = res.getExtensionClasses().contains("DefaultErrorView");
                    if (errPage) {
                        System.out.print(attrAdded ? ", error page" : "error page");
                    } else {
                        System.out.print(attrAdded ? ", page" : "page");
                    }
                    attrAdded = true;
                }
                if (!errPage && (parentRedirect || parentSecured)) {
                    System.out.print(attrAdded ? "; inherited: " : "inherited: ");
                    System.out.print(parentRedirect ? "redirect" : "");
                    System.out.print(parentSecured ? parentRedirect ? ", secured" : "secured" : "");
                    attrAdded = true;
                }

                System.out.print(attrAdded ? " | " : "");
                if (res.getAssociatedFile().exists()) {
                    System.out.print(res.isFolder() ? "dir: " : "file: ");
                } else {
                    System.out.print(res.isFolder() ? "no dir, missing: " : "no file, missing: ");
                }
                System.out.println(getRelativePath(project.getProjectRoot().getUnderlyingResourceObject(), res.getAssociatedFile()));
                System.out.print(")");
            }

            printViewConfig(res.getMetadataChildList(), pagesOnly, namePath, redirect || parentRedirect, secured || parentSecured);
        }
    }


    @Command(value = "viewconfig-list-outdated", help = "List all outdated ViewConfig classes and missing '.xhtml' files")
    public void viewConfigListOutdated(final PipeOut out) {
        if (!isInstalled()) {
            return;
        }

        boolean upToDate = true;
        for (ViewConfigUtil viewConfig : ViewConfigUtil.getAllViewConfigs(shell)) {
            List<ViewConfigMetadata> outdatedPages = getOutdatedPages(viewConfig.getViewConfigMetadata());
            if (!outdatedPages.isEmpty()) {
                System.out.print("[" + viewConfig.getViewConfigMetadata().getName() + "] ViewConfig is outdated. ");
                System.out.println("The following files are missing:");
                for (ViewConfigMetadata page : outdatedPages) {
                    System.out.println("  - " + getRelativePath(project.getProjectRoot().getUnderlyingResourceObject(), page.getAssociatedFile()));
                }
                upToDate = false;
            }
        }

        if (upToDate) {
            System.out.println("All ViewConfigs are up to date!");
        }

    }

    private List<ViewConfigMetadata> getOutdatedPages(ViewConfigMetadata vcMetadata) {

        AnnotationHelper annotationHelper = new AnnotationHelper();
        if (!vcMetadata.isFolder() &&
                !vcMetadata.getAssociatedFile().exists() &&
                !annotationHelper.hasAnnotation(vcMetadata, Deprecated.class)) {
            return Arrays.asList(vcMetadata);
        }

        List<ViewConfigMetadata> outdatedPages = new ArrayList<ViewConfigMetadata>();
        for (ViewConfigMetadata res : vcMetadata.getMetadataChildList()) {
            outdatedPages.addAll(getOutdatedPages(res));
        }

        return outdatedPages;
    }

    private String getRelativePath(File rootFile, File currentFile) {
        return "/" + rootFile.toURI().relativize(currentFile.toURI()).getPath();
    }

    private InputStream getViewTemplate(String viewName) {
        int index = viewName.lastIndexOf(".");

        if (index == -1) {
            ShellMessages.error(out, "View '" + viewName + "' is not a valid file type.");
            return null;
        }

        String viewExtension = viewName.substring(index + 1);
        switch (ViewType.from(viewExtension)) {
            case HTML:
                return getClass().getResourceAsStream("/org/apache/myfaces/extensions/cdi/forge/web/view.html");
            case XHTML:
                return getClass().getResourceAsStream("/org/apache/myfaces/extensions/cdi/forge/web/view.xhtml");
            case JSP:
                return getClass().getResourceAsStream("/org/apache/myfaces/extensions/cdi/forge/web/view.jsp");
            default:
                ShellMessages.info(out, "Unknown view type: " + viewName);
                return null;
        }
    }

    private boolean isInstalled() {
        if (!project.hasFacet(CodiFacet.class)) {
            ShellMessages.error(out, "Codi is not installed. Use [codi setup] to get started.");
            return false;
        }

        return true;
    }

    private class DefaultFolder {

        private ViewConfigUtil defaultViewConfig;
        private ViewConfigMetadata defaultMetadata;

        private DefaultFolder(ViewConfigUtil defaultViewConfig, ViewConfigMetadata defaultMetadata) {
            this.defaultViewConfig = defaultViewConfig;
            this.defaultMetadata = defaultMetadata;
        }

        public ViewConfigUtil getDefaultViewConfig() {
            return defaultViewConfig;
        }

        public ViewConfigMetadata getDefaultMetadata() {
            return defaultMetadata;
        }
    }

}
